Загрузка изображений¶
Читаю изображения с помощью функции imread и проверяю, всё ли на месте. Соберу всю в массив images, чтобы по возможности поменьше повторяться.
Потом я пишу функцию для показа изображений на основе matplotlib, предварительно переведя из BRG в RGB, чтобы всё не выглядело по инопланетянски.
import cv2
import numpy as np
import matplotlib.pyplot as plt
tomatoes_img = cv2.imread('./images/tomatoes.jpg')
street_img = cv2.imread('./images/street.jfif')
dog_img = cv2.imread('./images/pes.jfif')
images = [tomatoes_img, street_img, dog_img]
titles = ['Помидоры', 'Улица', 'Пееес']
for img in images:
if img is None:
print('А где...')
else:
print('Что-то да есть')
Что-то да есть Что-то да есть Что-то да есть
def show_images(images, titles=None, cols=3, figsize=(12, 4)):
"""
Отображает список изображений (цветных или grayscale) в сетке.
Параметры:
----------
images : list
Список изображений (в формате BGR, RGB или grayscale).
titles : list[str]
Заголовки для каждого изображения (может быть None).
cols : int
Количество столбцов для вывода.
figsize : tuple
Размер всей фигуры matplotlib (ширина, высота).
"""
n = len(images)
rows = (n + cols - 1) // cols
plt.figure(figsize=figsize)
for i, img in enumerate(images):
plt.subplot(rows, cols, i + 1)
if img is None:
plt.text(0.5, 0.5, "⚠️ None", ha="center", va="center", fontsize=12)
plt.axis("off")
continue
# Если изображение цветное (3 канала)
if len(img.shape) == 3:
# OpenCV загружает в BGR — меняем порядок на RGB
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
else:
# grayscale (2D)
plt.imshow(img, cmap='gray')
if titles and i < len(titles):
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
show_images(images, titles)
Преобразование в оттенки серого¶
Погнали. Первый способ - усреднение каналов. Надо пройтись по каждому пикселёчку, взять значения каналов R, G, B и найти их среднее значение и заменить на него значения всех каналов. С этим отлично справится функция mean из пакета numpy. Работать будем на 2 оси, так как на 0 строка, а на 1 столбцы. Это значение будет во float, поэтому надо будет перевести его в int.
С помощью ранее написанной функции выведем весь этот траур на экран.
images_bw = []
for img in images:
if img is not None:
gray = np.mean(img, axis=2).astype(np.uint8)
images_bw.append(gray)
show_images(images_bw, titles)
Второй способ - через модель HSV. H - цвет, S - насыщенность, V - яркость. Просто переведём изображения в этот формат и выведем насыщенность и цвет в 0, оставив только яркость. Для этого используем функцию split, чтобы управлять каналами по отдельности.
images_bw_hsv = []
for img in images:
hsv_image = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(img)
images_bw_hsv.append(v)
show_images(images_bw_hsv)
Бинаризация¶
Делаем пиксели либо черными, либо белыми, а где будет порог - решит Отсу. Для этого исопльзуем функцию threshold, который вернет значение порога и само изображение. Туда передадим ранее созданные чб изображения, 255 как максимальное значение (белый) и то, что мы будем находить порог с помощью метода Отсу, а не проставлять самостоятельно.
binary_images = []
for img in images_bw:
thresh_val, binary_otsu = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)
binary_images.append(binary_otsu)
print('Порог: ', thresh_val)
show_images(binary_images)
Порог: 140.0 Порог: 125.0 Порог: 102.0
Нормализация, эквализация, растяжение гистограммы¶
Нормализация, эквализация, растяжение гистограммы - способы улучшения изображения. Сама гистограмма показывает распределение уровня яркости пикселей от 0 до 255. Если в гистограмме всё более менее распределено равномерно, значит, изображение клёвое контрастное, а если значения сосредоточены в одной области - разобрать происходящее сложнее.
Начнём с нормализации - можно сказать, мы просто "растягиваем" гистограмму. В openCV это делается с помощью функции normalize. Используем линейное растяжение и диапазон от 0 до 255.
Для простоты сравнения с вывела сначала оригинал, потом нормализованные картинки. Они стали чуть контрастнее.
def show_histograms(images, titles=None):
n = len(images)
plt.figure(figsize=(12, 4))
for i in range(n):
plt.subplot(1, n, i + 1)
plt.hist(images[i].ravel(), bins=256, range=[0, 256], color='gray')
if titles:
plt.title(titles[i])
else:
plt.title(f"Гистограмма {i+1}")
plt.xlabel('Яркость пикселя')
plt.ylabel('Кол-во пикселей')
plt.tight_layout()
plt.show()
normalized_images = []
for img in images_bw:
normalized = cv2.normalize(img, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX)
normalized_images.append(normalized)
show_images(images_bw)
show_histograms(images_bw)
show_images(normalized_images)
show_histograms(normalized_images)
Растяжение - насколько я поняла, такой же концепт, но тут можно самостоятельно выбирать диапазон и растягивать куски выборочно. Делается по формуле без функции в библиотеке.
stretched_images = []
for img in images_bw:
min_val = np.min(img)
max_val = np.max(img)
stretched = ((img - min_val) / (max_val - min_val) * 255).astype(np.uint8)
stretched_images.append(stretched)
show_images(images_bw)
show_histograms(images_bw)
show_images(stretched_images)
show_histograms(stretched_images)
Последний метод - эквализация, чуть более хитрый. Он вычисляет гитограмму и находит кумулятивную функцию распределения, после чего перераспределяет яркость. Он, слава богу, находится под капотом openCV в методе equalizeHist.
equalized_images = []
for img in images_bw:
equalized = cv2.equalizeHist(img)
equalized_images.append(equalized)
show_images(images_bw)
show_histograms(images_bw)
show_images(equalized_images)
show_histograms(equalized_images)
Операции свёртки с различными масками¶
Размытие (Гауссиан)¶
Гауссово размытие нужно для того, чтобы сделать изображение более гладким, уменьшив количество шума. Используется фильтр, основанный на нормальном (гауссовом) распределении — “колокольчик”. Каждый пиксель заменяется взвешенным средним его соседей. Пиксели ближе к центру имеют больший вес (влияют сильнее), дальние — меньше. В openCV это делается с помощью функции GaussianBlur. В функции есть 2 важных параметра, влияющие на силу размытия - размер ядра (сколько пикселей в окрестности будут влиять на этот пиксель) и sigma - то, насколько быстро веса уменьшаются от центра к краю.
blurred_images = []
for img in images:
blur = cv2.GaussianBlur(img, (7, 7), 1.5)
blurred_images.append(blur)
show_images(images)
show_images(blurred_images)
Повышение четкости (Лаплассиан и его производные)¶
Теперь мы наоборот хотим сделать изображение более четким. Для этого мы хотим найти, где яркость изображения меняется (контуры) и сделать их более контрастными. Для этого надо найти места резких изменений в изменении яркости, т.е. вторую производную. Используем функцию Laplacian из openCV. Там есть 2 параметра - ddepth (глубина выходного изображения) и ksize. Это размер ядра (1, 3, 5). Чем больше — тем сильнее “реагирует” на шум. Альфа - коэффициент усиления. После неё избавляемся от отрицательных значений и приводит к инту с помощью convertScaleAbs.
sharp_images = []
alpha = 1.0
for img in blurred_images:
lap = cv2.Laplacian(img, cv2.CV_64F)
sharp = cv2.convertScaleAbs(img - alpha * lap)
sharp_images.append(sharp)
show_images(blurred_images)
show_images(sharp_images)
Выделение краёв (оператор Собеля)¶
Тут мы хотим найти границы и контур объектов. Для этого смотрим, как меняется яркость с помощью производной. Затем мы берем специальные матрицы свертки, которые умножаются (сворачиваются) с изображением, и в результате получаем два новых изображения - где контуры по вертикали и по горизонтали. Для этого в openCV есть функция Sobel, в которую передаются глубина выходных данных (cv2.CV_64F, чтобы не терять отрицательные значения), порядок производной по оси X (1 — первая производная), порядок производной по оси Y и размер ядра (1, 3, 5, 7) — чем больше, тем “грубее” контуры.
Затем контуры надо объединить. Вычисляем модуль градиента (корень из суммы квадратов). Для этого есть функция magnitude.
sobel_images = []
for img in blurred_images:
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel = cv2.magnitude(sobel_x, sobel_y)
sobel_uint8 = cv2.convertScaleAbs(sobel)
sobel_images.append(sobel_uint8)
show_images(blurred_images, ["Blurred 1", "Blurred 2", "Blurred 3"])
show_images(sobel_images, ["Sobel 1", "Sobel 2", "Sobel 3"])
shifted_images = []
dx = 50 # сдвиг по горизонтали (вправо)
dy = 30 # сдвиг по вертикали (вниз)
for img in images:
shifted = np.roll(img, shift=(dy, dx), axis=(0, 1))
shifted_images.append(shifted)
show_images(images, ["Image 1", "Image 2", "Image 3"])
show_images(shifted_images, [f"Shifted 1", f"Shifted 2", f"Shifted 3"])
Поворот изображения относительно произвольного центра¶
Чтобы повернуть изображение на угол θ вокруг произвольного центра (cx, cy), нам нужна аффинная матрица трансформации. В OpenCV это делается функцией getRotationMatrix2D, куда передаются центр поворота, угол в градусах (положительный = по часовой стрелке) и масштаб (1.0 = без изменения размера).
rotated_images = []
angle = 45 # угол поворота в градусах
scale = 1.0 # масштаб (1.0 = без изменения размера)
for img in images:
h, w = img.shape[:2]
center = (w//2, h//2)
M = cv2.getRotationMatrix2D(center, angle, scale)
rotated = cv2.warpAffine(img, M, (w, h))
rotated_images.append(rotated)
show_images(images, ["Image 1", "Image 2", "Image 3"])
show_images(rotated_images, [f"Rotated 1", f"Rotated 2", f"Rotated 3"])